Service Worker:离线应用与后台同步的解决方案
前端常用缓存技术
前端常用缓存技术一般分为http缓存和浏览器缓存。
HTTP缓存
Expires
HTTP1.0的内容,服务器使用Expires头来告诉Web客户端它可以使用当前副本,直到指定的时间为止;
Cache-Control
HTTP1.1引入了Cathe-Control,它使用max-age指定资源被缓存多久,主要是解决了Expires一个重大的缺陷,就是它设置的是一个固定的时间点,客户端时间和服务端时间可能有误差;
Last-Modified / If-Modified-Since
Last-Modified是服务器告诉浏览器该资源的最后修改时间,If-Modified-Since是请求头带上的,上次服务器给自己的该资源的最后修改时间。然后服务器拿去对比。
若Last-Modified大于If-Modified-Since,说明资源有被改动过,则响应整片资源内容,返回状态码200;
若Last-Modified小于或等于If-Modified-Since,说明资源无新修改,则响应HTTP 304,告知浏览器继续使用当前版本。
Etag / If-None-Match
Etag是服务器根据每个资源生成的唯一标识符,当文件内容被修改时标识符就会重新生成。服务器存储着文件的Etag字段,可以在与每次客户端传送If-none-match的字段进行比较。如果相等,则表示未修改,响应304;反之,则表示已修改,响应200状态码,返回数据。
浏览器缓存
Storage
简单的缓存方式有cookie,localStorage和sessionStorage,都是浏览器内置储存功能。
mainfest
html5引入的新标准,可以离线缓存静态文件。
Service Worker
ServiceWorker 介绍
什么是ServiceWorker
ServiceWorker特性
Service Worker本质上是一个Web Worker,它独立于Javascript主线程,因此它不能直接访问DOM,也不能直接访问window对象,但是可以访问navigator对象,也可以通过消息传递的方式(如postMessage)与Javascript主线程进行通信。
Service Worker独立于Javascript主线程,所以不会造成阻塞。它设计为完全异步,同步API(如XHR和localStorage不能在Service Worker中使用。
Service Worker是基于 HTTPS 的,因为Service Worker中涉及到请求拦截,所以必须使用HTTPS协议来保障安全。如果是本地调试的话,localhost是可以的。
Service Worker拥有独立的生命周期,与页面无关(关联页面未关闭时,它也可以退出,没有关联页面时,它也可以启动)。注册Service Worker后,浏览器会默默地在背后安装Service Worker。
ServiceWorker生命周期
1. Parsed
当我们第一次尝试注册 Service Worker 时,用户代理会解析脚本并获取入口点。如果解析成功(并且满足其他一些要求,例如 HTTPS),我们将可以访问 Service Worker 注册对象。其中包含有关 Service Worker 的状态及其作用域的信息。
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js')
.then(function(registration) {
console.log("Service Worker Registered", registration);
})
.catch(function(err) {
console.log("Service Worker Failed to Register", err);
})
}
Service Worker注册成功并不意味着它已安装完毕或处于激活状态,而只是意味着脚本已成功解析,它与文档处于同一源上,且源为 HTTPS。注册完成后,服务 Worker 将进入下一个状态。
2. Installing
一旦 Service Worker 脚本被解析,用户代理就会尝试安装它,并进入安装状态。在 Service Worker 的registration对象中,我们可以在installing属性中检查此状态。
并且,在installing状态下,install事件会被触发,我们一般会在这个回调中处理缓存事件。
navigator.serviceWorker.register('./sw.js').then(function(registration) {
if (registration.installing) {
// Service Worker is Installing
}
})
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(currentCacheName).then(function(cache) {
return cache.addAll(arrayOfFilesToCache);
})
);
});
如果事件中有 event.waitUntil() 方法,其中的 Promise只有在resolve后,install事件才会成功。如果 Promise 被拒绝,install就会失败,Service Worker 就会变为redundant状态。
self.addEventListener('install', function(event) {
event.waitUntil(
return Promise.reject(); // Failure
);
});
3. Installed / Waiting
如果安装成功,Service Worker 的状态变为 installed (也叫 waiting )。处于这个状态时, Service Worker 是有效的但是是未激活的 worker,暂时没有控制页面的权力,需要等待从当前 worker 获得控制权。
我们可以在 registration 对象的 waiting 属性中检测到此状态。
navigator.serviceWorker.register('./sw.js').then(function(registration) {
if (registration.waiting) {
// Service Worker is Waiting
}
})
我们可以在这个时机去更新新版本或自动更新缓存。
4. Activating
在以下情况之一时,处于 Waiting 状态的 worker 的 Activating 状态会被触发:
当前没有处于激活状态的 worker
self.skipWaiting() 在 sw.js 中被调用,直接跳过waiting阶段
用户导航离开当前页面,从而释放了前一个 active worker
经过了指定时间段,从而释放了前一个 active worker
在当前状态下,activate事件会被触发,在这个回调中我们通常用于清除旧缓存。
self.addEventListener('activate', function(event) {
event.waitUntil(
// Get all the cache names
caches.keys().then(function(cacheNames) {
return Promise.all(
// Get all the items that are stored under a different cache name than the current one
cacheNames.filter(function(cacheName) {
return cacheName != currentCacheName;
}).map(function(cacheName) {
// Delete the items
return caches.delete(cacheName);
})
); // end Promise.all()
}) // end caches.keys()
); // end event.waitUntil()
});
同install事件,如果Promise被reject了,则activate事件失败,Service Worker变为redundant状态。
5. Activated
如果激活成功,Service Worker 状态会变成 active ,在这个状态下,Service Worker 是一个可以完全控制网页的激活 worker,我们可以在 registration 对象的 active 属性中检测到此状态。
navigator.serviceWorker.register('./sw.js').then(function(registration) {
if (registration.active) {
// Service Worker is Active
}
})
当 Service Worker 被成功激活后,即可处理绑定的 fetch 和 message 事件。
self.addEventListener('fetch', function(event) {
// Do stuff with fetch events
});
self.addEventListener('message', function(event) {
// Do stuff with postMessages received from document
});
6. Redundant
以下任一情况,Service Worker 都会变成 redundant。
install失败
activate失败
有新的 Service Worker 将其替代成为现有的激活 worker
Service Worker 离线缓存
Service Worker 最重要的功能之一,就是可以通过缓存静态资源来实现离线访问我们的页面。
Service Worker 的缓存基于 CacheStorage,它是一个 Promise 对象,我们可以通过 caches 来获取它。CacheStorage提供了一些方法,我们可以通过这些方法来对缓存进行操作。
caches.open(currentCacheName).then(function (cache) {
/** 可以通过cache.put来添加缓存
* 它接收两个参数,第一个参数是Request对象或URL字符串,第二个参数是Response对象
*/
cache.put(new Request('/'), new Response('Hello World'));
/** 可以通过cache.addAll来添加缓存资源数组
* 它接收一个参数,这个参数可以是Request对象数组,也可以是URL字符串数组
*/
cache.addAll(['/'])
/** 可以通过cache.match来获取缓存
* 它接收一个参数,这个参数可以是Request对象,也可以是URL字符串
*/
cache.match('/').then(function (response) {
console.log(response);
});
/** 可以通过cache.delete来删除缓存
* 它接收一个参数,这个参数可以是Request对象,也可以是URL字符串
*/
cache.delete('/').then(function () {
console.log('删除成功');
});
/** 可以通过cache.keys来获取缓存的key
* 然后通过cache.delete来删除缓存
*/
cache.keys().then(function (keys) {
keys.forEach(function (key) {
cache.delete(key);
});
});
});
缓存资源
我们在介绍生命周期的时候我们介绍了在installing状态下会调用install方法,通常我们会在install事件中缓存一些资源。
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open(currentCacheName).then(function (cache) {
return cache.addAll([
'/',
'/index.css',
'/axios.js',
'/index.html'
]);
})
);
});
上面的代码中我们缓存了一些资源,所以我们可以在fetch事件中获取并返回刚刚缓存的资源。
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
上面的代码中我们使用caches.match来匹配请求,如果匹配到了,那么就返回缓存的资源,如果没有匹配到,那么就从网络中获取资源。
缓存更新
在上面的步骤中,我们已经缓存了我们的资源,并且该资源并不会随着我们代码或者资源的更改而更新缓存。因此,我们可以通过版本号来控制更新。
介绍生命周期时,我们有了解到在activating状态下会触发activate回调,在该回调中我们可以清除旧缓存,然后在install事件中缓存新的资源。
const version = '2.0';
const currentCache = 'my-cache' + version;
self.addEventListener('activate', function (event) {
event.waitUntil(
caches.keys().then(function (cacheNames) {
return Promise.all(
cacheNames.map(function (cacheName) {
if (cacheName !== currentCache) {
return caches.delete(cacheName);
}
})
);
})
);
});
卸载
当我们的页面不再需要Service Worker的时候,可以通过在新版本里使用unregister进行卸载。
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
需要注意的是,Service Worker卸载并不会删掉我们之前缓存的资源,所以在卸载之前我们需要清除所有的缓存。
缓存策略
从上面的例子可以看出,Service Worker的缓存是通过Cache接口和fetch事件共同实现的。通过Cache接口和fetch事件可以实现多种缓存策略。
1. 仅缓存 (Cache only)
适用于你认为属于该“版本”网站静态内容的任何资源,匹配的请求将只会进入缓存。
2. 仅网络 (Network only)
与“仅缓存”相反,“仅限网络”是指请求通过 Service Worker 传递到网络,而无需与 Service Worker 缓存进行任何交互。
3. 缓存优先 (Cache first)
该策略流程如下:
请求到达缓存。如果资源位于缓存中,请从缓存中提供。
如果请求不在缓存中,请转到网络。
网络请求完成后,将其添加到缓存中,然后从网络返回响应。
该策略适用于静态资源的缓存,它可以绕过 HTTP 缓存可能启动的服务器执行任何内容新鲜度检查,从而加快不可变资源的速度。
4. 网络优先 (Network first)
该策略如下:
先前往网络请求一个请求,然后将响应放入缓存中。
如果您日后处于离线状态,则会回退到缓存中该响应的最新版本。
此策略非常适合 HTML 或 API 请求,当您想在线获取资源的最新版本,同时又希望离线可以访问到最新的可用版本。
5. 延迟验证 (Stale-while-revalidate)
该机制与最后两种策略类似,但其过程优先考虑资源访问速度,同时还在后台保持更新。策略大致如下:
在第一次请求获取资源时,从网络中提取资源,将其放入缓存中并返回网络响应。
对于后续请求,首先从缓存提供资源,然后“在后台”从网络重新请求该资源,并更新资源的缓存条目。
对于此后的请求,您将收到在上一步中从缓存中放置的最后一个网络提取的版本。
Service Worker 后台同步
假设用户在我们的页面上操作了数据并提交,此时正好进入一个网络极差甚至断网的环境里,用户只能看着一直处于loading状态的页面,直到失去耐心关闭页面,这时请求就已经被中断了。
上面这种情况暴露了两个问题:
普通页面会随着页面关闭而终止
网络极差或无网络情况下没用一种解决方案能够解决并维持当前请求以待有网时恢复请求
后台同步是构建在 Service Worker 进程之上的另一个功能,它允许一次性或以一个时间间隔请求后台数据同步。我们可以充分利用这一功能规避以上问题。
工作流程
在Service Worker中监听sync事件
在浏览器中发起后台同步sync
就会触发Service Worker的sync事件,在该监听的回调中进行操作,例如向后端发起请求
然后可以在Service Worker中对服务端返回的数据进行处理
1. 页面触发同步
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js')
navigator.serviceWorker.ready.then(function (registration) {
let tag = "data_sync";
document.getElementById('submit-btn').addEventListener('click', function () {
registration.sync.register(tag).then(function () {
console.log('后台同步已触发', tag);
}).catch(function (err) {
console.log('后台同步触发失败', err);
});
});
})
}
由于后台同步功能需要在Service Worker注册完成后触发,所以我们可以使用navigator.serviceWorker.ready等待注册完成准备好之后使用 registration.sync.register 注册同步事件。
registration.sync 会返回一个SyncManager对象其中包含register方法和getTags方法。
2. SW监听同步事件
当页面触发同步事件后,我们需要通过Service Worker来处理sync事件。
self.addEventListener('sync', function (e) {
let init = { method: 'GET' };
switch (e.tag){
case "data_sync":
let request = new Request(`xxxxx/sync`, init);
e.waitUntil(
fetch(request).then(function (response) {
return response;
})
);
break;
}
});
Taro项目集成
理论说完了,接下来我们可以在taro项目里实践接入Service Worker。
俗话说,站在巨人的肩膀上看世界。
现在市面上实现SW的工具非常多,其中google团队提供了一个十分强大且完善的插件 workbox-webpack-plugin ,接下来我们将通过这个插件实现Service Worker的离线缓存功能。
插件配置
workbox-webpack-plugin提供了两个类名为 GenerateSW 和 InjectManifest,接下来我们通过使用GenerateSW来实现预缓存文件和简单的运行时缓存需求。
在taro项目负责打包的config文件中加入以下配置:
const { GenerateSW } = require('workbox-webpack-plugin');
const config = {
...
h5: {
...
webpackChain(chain) {
...
chain.plugin('generateSW').use(new GenerateSW({
clientsClaim: true,
skipWaiting: true,
runtimeCaching: [
{
urlPattern: /.\/*/, // 需要缓存的路径
handler: 'StaleWhileRevalidate', // 缓存策略
options: {
cacheName: 'my-webcache',
expiration: {
maxEntries: 2000,
},
},
}],
}));
}
}
}
加入以上配置后,我们运行build命令可以发现该插件为我们自动生成了Service Worker文件。
Service Worker注册
生成Service Worker文件之后我们需要在项目中进行注册。
在register文件中处理Service Worker的生命周期、状态等信息。
import { register } from 'register-service-worker';
register('./service-worker.js', {
registrationOptions: { scope: './' },
ready(registration) {
console.log('Service worker is active.', registration);
},
registered(registration) {
console.log('Service worker has been registered.', registration);
},
cached(registration) {
console.log('Content has been cached for offline use.', registration);
},
updatefound(registration) {
console.log('New content is downloading.', registration);
},
updated(registration) {
console.log('New content is available; please refresh.', registration);
},
offline() {
console.log('No internet connection found. App is running in offline mode.');
},
error(error) {
console.error('Error during service worker registration:', error);
},
});
在app.ts中引入该文件,我们就完成了简单的Service Worker的引入。接下来把项目启动,让我们看看SW是否生效。
在正常网络环境中,可以看到我们发起第一次访问的请求列表。
在把网络设置成离线状态后,可以看到我们的请求依然正常返回,并走的是Service Worker的缓存。
我们也可以在控制台看到所有缓存的文件列表。
总的来说,Service Worker是一个非常强大的功能,除了以上介绍的离线缓存和后台同步功能,还可以通过SW实现消息推送、多页面通信等等功能。
The End
如果你觉得这篇内容对你挺有启发,请你轻轻点下小手指,帮我两个小忙呗:
1、点亮「在看」,让更多的人看到这篇满满干货的内容;
2、关注公众号「哈啰技术」,可第一时间收到最新技术推文。
如果喜欢就点个👍喔,有您的喜欢⛽,我们会更有动力输出有价值的技术分享滴。